React
setState
Good read: https://chatgpt.com/share/79fab4bb-a4a9-4a58-8e55-51d216369c16
React's state update is asynchronous, most of the time it is fine but sometimes it creates some headache due its asynchronous + batching behavior
setState generally has 2 phases: State update & Re-render
When we call setState -> Schedule State Update in the [[Event Loop#^f7064a | Task Queue]] and mark the component as dirty
- All these queued State Update will be batched and execute in 1 [[Event Loop#^f7064a | Event Loop]]
- After executing the batched State Update, it will schedule the Re-render (also in the [[Event Loop#^f7064a | Task Queue]]) to be picked up by the next available iteration of the [[Event Loop#^f7064a | Event Loop]]
setState has 2 form: functional & non-functional
There's no change in scheduling mechanism between 2 forms
When using functional form (updater function), it guarantees to receive the most updated state (event with batching) as the state will be applied sequentially
setState(prevState => ({ count: prevState.count + 1 })); // First call
setState(prevState => ({ count: prevState.count + 1 })); // Second call
When both of these
setState
calls are batched:- React doesn’t execute them immediately.
- It first processes the first updater function:
prevState.count
is the currentcount
value (say,count = 0
), so it calculatescount + 1 = 1
and updates the state to{ count: 1 }
.
- It then processes the second updater function:
- This time,
prevState.count
is 1 (the result of the first updater), so it calculatescount + 1 = 2
and updates the state to{ count: 2 }
.
- This time,
At the end of the batch, React will have updated the state correctly, and only one re-render will happen with the new state
{ count: 2 }
.When you use non-functional
setState
calls (e.g.,setState({ count: 1 })
), React simply overwrites the state with the new object. But, if multiplesetState
calls occur in the same event, React can batch these updates as well. In this case, the last update wins, and intermediate updates may be lost if they occur before the batch is processed.
setState({ count: 1 }); // Direct update
setState({ count: 2 }); // Overwrites previous update
- In this case, React will batch these, and only the last state
{ count: 2 }
is applied, resulting in one re-render withcount = 2
.
Deep Dive: React State Capture Patterns
The Challenge with React State Updates
When working with React state, we often need to both update the UI and capture state values at specific moments. This can be tricky due to React's asynchronous state updates.
Three Common Approaches (And Why Only One Works)
1. Direct Console Log After setState (❌ Doesn't Work)
setState(prev => [...prev, newItem])
console.log(state) // Shows old state!
This fails because:
- React state updates are asynchronous
- The console.log runs immediately, before React updates the state
- You'll always see the state value from before the update
2. Console Log Inside setState (⚠️ Partially Works)
setState(prev => {
const newState = [...prev, newItem]
console.log('Inside updater:', newState) // Works, but trapped in the updater
return newState
})
This partially works because:
- You can see the new state value inside the updater function
- BUT the value is trapped inside the updater
- Can't use this value outside for API calls or other operations
3. Variable Capture Attempt (❌ Doesn't Work)
let capturedState = []
setState(prev => {
capturedState = prev.slice() // This assignment happens later!
return [...prev, newItem]
})
console.log(capturedState) // Still empty! Runs before the assignment
This fails because:
- Even though we assign to
capturedState
in the updater - The updater function runs on React's schedule
- The console.log runs immediately, before the updater executes
- The issue here is that React state updates are asynchronous. When you call setState, React schedules the update but doesn't apply it immediately. So if you try to access capturedState right after setState, you'll still get the old value because the state hasn't been updated yet.
The Promise Pattern Solution (✅ Works)
Basic Implementation
async function handleStateUpdate() {
const capturedState = await new Promise(resolve => {
setState(prevState => {
const currentState = prevState.slice() // Capture current state
resolve(currentState) // Make it available outside
return [...prevState, newItem] // Update UI
})
})
// Now we can use capturedState however we want!
console.log('Captured state:', capturedState)
await apiCall(capturedState)
}
Detailed Execution Flow
- Promise executor runs synchronously
setState
updater function runs synchronouslyresolve()
captures the value we want synchronously- React schedules the state update independently
await
ensures we wait for our captured value
// What happens in order:
const captured = await new Promise(resolve => {
// 1️⃣ This runs NOW
setState(prev => {
// 2️⃣ This also runs NOW
const valueToCapture = prev.slice()
resolve(valueToCapture) // 3️⃣ This runs NOW
return newState // 4️⃣ React schedules this update
})
})
// 5️⃣ We wait here until resolve() completes
console.log(captured) // 6️⃣ Now we have our value!
This is why the Promise approach works better - it gives us control over exactly what state we want to capture and when we want to use it, regardless of React's asynchronous state update timing.
Think of it like taking a snapshot:
- The first approach is like trying to take a picture of something that's changing - you might miss the moment
- The Promise approach is like having your camera ready and capturing exactly the moment you want, then being able to look at that picture later
This pattern is particularly useful when you need to:
- Capture a specific moment in your state's lifecycle
- Use that captured value after the state has changed
- Ensure you have the exact data you want, regardless of React's state update timing
Real-World Example: Chat Application
Here's a practical example showing why this pattern is useful:
async function handleNewMessage(newMessage) {
// 1. Capture existing messages before adding new one
const previousMessages = await new Promise(resolve => {
setMessages(prev => {
resolve(prev.slice()) // Capture current messages
// Update UI with new message + loading state
return [...prev,
{ text: newMessage, sender: 'user' },
{ text: '...typing', sender: 'bot', loading: true }
]
})
})
try {
// 2. Send only previous messages to API
const response = await api.sendMessage({
history: previousMessages, // Use captured state
newMessage: newMessage
})
// 3. Update the loading message with response
setMessages(prev => {
const current = prev.slice()
current[current.length - 1] = {
text: response.text,
sender: 'bot',
loading: false
}
return current
})
} catch (error) {
// Handle error...
}
}
Why This Pattern is Powerful
Timing Control
- Captures state exactly when we need it
- Not affected by React's update scheduling
- Guarantees we have the right value at the right time
Independent Operations
const captured = await new Promise(resolve => {
setState(prev => {
resolve(prev) // Capture happens independently
return newState // State update happens independently
})
})- Promise resolution is separate from state update
- React's state management remains unchanged
- No interference with React's batching or scheduling
Flexible Usage
const captured = await new Promise(resolve => {
setState(prev => {
// Can capture anything we want
resolve({
original: prev,
timestamp: Date.now(),
computed: computeSomething(prev)
})
return newState
})
})
Important Distinctions
This is NOT changing React's behavior
- State updates still happen on React's schedule
- We're just creating a way to capture values synchronously
- React's performance optimizations remain intact
Think of it like Photography
- Regular state access: Trying to take a picture of something moving
- Promise pattern: Capturing the exact moment we want, while letting the motion continue
When to Use This Pattern
- Need exact state at a specific moment
- Want to use state values in async operations
- Need to maintain UI responsiveness while processing state
- Need to send previous state to APIs while showing optimistic UI updates
This pattern provides a reliable way to work with React's asynchronous state updates while maintaining proper timing and state management, especially useful in complex scenarios involving API calls or state-dependent operations.